#!/usr/bin/python

# Copyright (c) 2016, Hugh Ma <Hugh.Ma@flextronics.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations

DOCUMENTATION = r"""
module: stacki_host
short_description: Add or remove host to stacki front-end
description:
  - Use this module to add or remove hosts to a stacki front-end using API.
  - Information on stacki can be found at U(https://github.com/StackIQ/stacki).
extends_documentation_fragment:
  - community.general.attributes
attributes:
  check_mode:
    support: none
  diff_mode:
    support: none
options:
  name:
    description:
      - Name of the host to be added to Stacki.
    required: true
    type: str
  stacki_user:
    description:
      - Username for authenticating with Stacki API, but if not specified, the environment variable E(stacki_user) is used
        instead.
    required: true
    type: str
  stacki_password:
    description:
      - Password for authenticating with Stacki API, but if not specified, the environment variable E(stacki_password) is
        used instead.
    required: true
    type: str
  stacki_endpoint:
    description:
      - URL for the Stacki API Endpoint.
    required: true
    type: str
  prim_intf_mac:
    description:
      - MAC Address for the primary PXE boot network interface.
      - Currently not used by the module.
    type: str
  prim_intf_ip:
    description:
      - IP Address for the primary network interface.
      - Currently not used by the module.
    type: str
  prim_intf:
    description:
      - Name of the primary network interface.
      - Currently not used by the module.
    type: str
  force_install:
    description:
      - Set value to V(true) to force node into install state if it already exists in stacki.
    type: bool
    default: false
  state:
    description:
      - Set value to the desired state for the specified host.
    type: str
    choices: [absent, present]
    default: present
  appliance:
    description:
      - Appliance to be used in host creation.
      - Required if O(state=present) and host does not yet exist.
    type: str
    default: backend
  rack:
    description:
      - Rack to be used in host creation.
      - Required if O(state=present) and host does not yet exist.
    type: int
    default: 0
  rank:
    description:
      - Rank to be used in host creation.
      - In Stacki terminology, the rank is the position of the machine in a rack.
      - Required if O(state=present) and host does not yet exist.
    type: int
    default: 0
  network:
    description:
      - Network to be configured in the host.
      - Currently not used by the module.
    type: str
    default: private
author:
  - Hugh Ma (@bbyhuy) <Hugh.Ma@flextronics.com>
"""

EXAMPLES = r"""
- name: Add a host named test-1
  community.general.stacki_host:
    name: test-1
    stacki_user: usr
    stacki_password: pwd
    stacki_endpoint: url
    prim_intf_mac: mac_addr
    prim_intf_ip: x.x.x.x
    prim_intf: eth0

- name: Remove a host named test-1
  community.general.stacki_host:
    name: test-1
    stacki_user: usr
    stacki_password: pwd
    stacki_endpoint: url
    state: absent
"""


import json

from ansible.module_utils.basic import AnsibleModule, env_fallback
from urllib.parse import urlencode
from ansible.module_utils.urls import fetch_url


class StackiHost:
    def __init__(self, module):
        self.module = module
        self.hostname = module.params["name"]
        self.rack = module.params["rack"]
        self.rank = module.params["rank"]
        self.appliance = module.params["appliance"]
        self.prim_intf = module.params["prim_intf"]
        self.prim_intf_ip = module.params["prim_intf_ip"]
        self.network = module.params["network"]
        self.prim_intf_mac = module.params["prim_intf_mac"]
        self.endpoint = module.params["stacki_endpoint"]

        auth_creds = {"USERNAME": module.params["stacki_user"], "PASSWORD": module.params["stacki_password"]}

        # Get Initial CSRF
        cred_a = self.do_request(self.endpoint, method="GET")
        cookie_a = cred_a.headers.get("Set-Cookie").split(";")
        init_csrftoken = None
        for c in cookie_a:
            if "csrftoken" in c:
                init_csrftoken = c.replace("csrftoken=", "")
                init_csrftoken = init_csrftoken.rstrip("\r\n")
                break

        # Make Header Dictionary with initial CSRF
        header = {
            "csrftoken": init_csrftoken,
            "X-CSRFToken": init_csrftoken,
            "Content-type": "application/x-www-form-urlencoded",
            "Cookie": cred_a.headers.get("Set-Cookie"),
        }

        # Endpoint to get final authentication header
        login_endpoint = f"{self.endpoint}/login"

        # Get Final CSRF and Session ID
        login_req = self.do_request(login_endpoint, headers=header, payload=urlencode(auth_creds), method="POST")

        cookie_f = login_req.headers.get("Set-Cookie").split(";")
        csrftoken = None
        for f in cookie_f:
            if "csrftoken" in f:
                csrftoken = f.replace("csrftoken=", "")
            if "sessionid" in f:
                sessionid = c.split("sessionid=", 1)[-1]
                sessionid = sessionid.rstrip("\r\n")

        self.header = {
            "csrftoken": csrftoken,
            "X-CSRFToken": csrftoken,
            "sessionid": sessionid,
            "Content-type": "application/json",
            "Cookie": login_req.headers.get("Set-Cookie"),
        }

    def do_request(self, url, payload=None, headers=None, method=None):
        res, info = fetch_url(self.module, url, data=payload, headers=headers, method=method)

        if info["status"] != 200:
            self.module.fail_json(changed=False, msg=info["msg"])

        return res

    def stack_check_host(self):
        res = self.do_request(
            self.endpoint, payload=json.dumps({"cmd": "list host"}), headers=self.header, method="POST"
        )
        return self.hostname in res.read()

    def stack_sync(self):
        self.do_request(self.endpoint, payload=json.dumps({"cmd": "sync config"}), headers=self.header, method="POST")
        self.do_request(
            self.endpoint, payload=json.dumps({"cmd": "sync host config"}), headers=self.header, method="POST"
        )

    def stack_force_install(self, result):
        data = {"cmd": f"set host boot {self.hostname} action=install"}
        self.do_request(self.endpoint, payload=json.dumps(data), headers=self.header, method="POST")
        changed = True

        self.stack_sync()

        result["changed"] = changed
        result["stdout"] = "api call successful".rstrip("\r\n")

    def stack_add(self, result):
        data = dict()
        changed = False

        data["cmd"] = f"add host {self.hostname} rack={self.rack} rank={self.rank} appliance={self.appliance}"
        self.do_request(self.endpoint, payload=json.dumps(data), headers=self.header, method="POST")

        self.stack_sync()

        result["changed"] = changed
        result["stdout"] = "api call successful".rstrip("\r\n")

    def stack_remove(self, result):
        data = dict()

        data["cmd"] = f"remove host {self.hostname}"
        self.do_request(self.endpoint, payload=json.dumps(data), headers=self.header, method="POST")

        self.stack_sync()

        result["changed"] = True
        result["stdout"] = "api call successful".rstrip("\r\n")


def main():
    module = AnsibleModule(
        argument_spec=dict(
            state=dict(type="str", default="present", choices=["absent", "present"]),
            name=dict(type="str", required=True),
            rack=dict(type="int", default=0),
            rank=dict(type="int", default=0),
            appliance=dict(type="str", default="backend"),
            prim_intf=dict(type="str"),
            prim_intf_ip=dict(type="str"),
            network=dict(type="str", default="private"),
            prim_intf_mac=dict(type="str"),
            stacki_user=dict(type="str", required=True, fallback=(env_fallback, ["stacki_user"])),
            stacki_password=dict(type="str", required=True, fallback=(env_fallback, ["stacki_password"]), no_log=True),
            stacki_endpoint=dict(type="str", required=True, fallback=(env_fallback, ["stacki_endpoint"])),
            force_install=dict(type="bool", default=False),
        ),
        supports_check_mode=False,
    )

    result = {"changed": False}
    missing_params = list()

    stacki = StackiHost(module)
    host_exists = stacki.stack_check_host()

    # If state is present, but host exists, need force_install flag to put host back into install state
    if module.params["state"] == "present" and host_exists and module.params["force_install"]:
        stacki.stack_force_install(result)
    # If state is present, but host exists, and force_install and false, do nothing
    elif module.params["state"] == "present" and host_exists and not module.params["force_install"]:
        result["stdout"] = f"{module.params['name']} already exists. Set 'force_install' to true to bootstrap"
    # Otherwise, state is present, but host doesn't exists, require more params to add host
    elif module.params["state"] == "present" and not host_exists:
        for param in ["appliance", "rack", "rank", "prim_intf", "prim_intf_ip", "network", "prim_intf_mac"]:
            if not module.params[param]:
                missing_params.append(param)
        if len(missing_params) > 0:
            module.fail_json(msg=f"missing required arguments: {missing_params}")

        stacki.stack_add(result)
    # If state is absent, and host exists, lets remove it.
    elif module.params["state"] == "absent" and host_exists:
        stacki.stack_remove(result)

    module.exit_json(**result)


if __name__ == "__main__":
    main()
